# frozen_string_literal: true

module Security
  module Ingestion
    module Tasks
      # Ingests the data for the `Vulnerabilities::Statistic` model.
      #
      # This ingestion task is neither `BulkInsertable` nor `BulkUpdatable` like
      # the rest of the ingestion tasks.
      # The data stored in the `vulnerability_statistics` table is incremental
      # therefore we need a different type of UPSERT query which is not supported
      # by ActiveRecord yet.
      class IngestVulnerabilityStatistics < AbstractTask
        TARGET_VALUES = "(TARGET.critical, TARGET.unknown, TARGET.high, TARGET.medium, TARGET.low)"
        EXCLUDED_VALUES = "(EXCLUDED.critical, EXCLUDED.unknown, EXCLUDED.high, EXCLUDED.medium, EXCLUDED.low)"
        UPSERT_SQL_TEMPLATE = <<~SQL
          INSERT INTO #{Vulnerabilities::Statistic.table_name} AS target (project_id, %{insert_attributes}, letter_grade, created_at, updated_at)
            VALUES (%{project_id}, %{insert_values}, %{letter_grade}, now(), now())
          ON CONFLICT (project_id)
            DO UPDATE SET
              %{update_values},
              letter_grade = (#{Vulnerabilities::Statistic.letter_grade_sql_for(TARGET_VALUES, EXCLUDED_VALUES)}),
              updated_at = now()
        SQL

        def execute
          return unless severity_counts.present?

          connection.execute(upsert_sql)
        end

        private

        delegate :project_id, to: :pipeline, private: true
        delegate :connection, to: ::Vulnerabilities::Statistic, private: true
        delegate :quote, :quote_column_name, to: :connection, private: true

        def upsert_sql
          format(
            UPSERT_SQL_TEMPLATE,
            project_id: project_id,
            insert_attributes: insert_attributes,
            insert_values: insert_values,
            letter_grade: letter_grade,
            update_values: update_values
          )
        end

        def insert_attributes
          sql_safe_severity_counts.keys.join(', ')
        end

        def insert_values
          sql_safe_severity_counts.values.join(', ')
        end

        # We have a `before_save` callback in `Vulnerabilities::Statistic` model
        # but the `before_save` callbacks are not invocated by the `BulkInsertSafe` module
        # so we have to calculate it here.
        def letter_grade
          Vulnerabilities::Statistic.letter_grade_for(severity_counts)
        end

        def update_values
          sql_safe_severity_counts.map { |severity, count| "#{severity} = TARGET.#{severity} + #{count}" }
                                 .join(', ')
        end

        def sql_safe_severity_counts
          @sql_safe_severity_counts ||= severity_counts.transform_keys { |key| quote_column_name(key) }
                                                       .transform_values { |value| quote(value) }
        end

        def severity_counts
          @severity_counts ||= finding_maps.select(&:new_record)
                                           .map(&:severity)
                                           .tally
        end
      end
    end
  end
end
